// Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.intellij.plugins.markdown.lang.psi.impl

import com.intellij.lang.ASTNode
import com.intellij.navigation.ColoredItemPresentation
import com.intellij.navigation.ItemPresentation
import com.intellij.openapi.editor.colors.TextAttributesKey
import com.intellij.openapi.options.advanced.AdvancedSettings
import com.intellij.openapi.util.registry.Registry
import com.intellij.openapi.util.text.StringUtil
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiElementVisitor
import com.intellij.psi.PsiReference
import com.intellij.psi.SyntaxTraverser
import com.intellij.psi.impl.source.resolve.reference.ReferenceProvidersRegistry
import com.intellij.psi.impl.source.tree.LeafPsiElement
import com.intellij.psi.util.CachedValueProvider
import com.intellij.psi.util.CachedValuesManager
import com.intellij.psi.util.PsiModificationTracker
import com.intellij.psi.util.elementType
import org.intellij.markdown.html.entities.Entities
import org.intellij.plugins.markdown.lang.MarkdownTokenTypeSets
import org.intellij.plugins.markdown.lang.psi.MarkdownElementVisitor
import org.intellij.plugins.markdown.lang.psi.util.children
import org.intellij.plugins.markdown.lang.psi.util.childrenOfType
import org.intellij.plugins.markdown.lang.psi.util.hasType
import org.intellij.plugins.markdown.lang.stubs.impl.MarkdownHeaderStubElement
import org.intellij.plugins.markdown.lang.stubs.impl.MarkdownHeaderStubElementType
import org.intellij.plugins.markdown.structureView.MarkdownStructureColors
import org.jetbrains.annotations.ApiStatus
import javax.swing.Icon

/**
 * Corresponds to both ATX and SETEXT headers.
 *
 * Use [contentElement] to agnostically obtain a content element for current header.
 */
@Suppress("DEPRECATION")
class MarkdownHeader: MarkdownHeaderImpl {
  constructor(node: ASTNode): super(node)
  constructor(stub: MarkdownHeaderStubElement, type: MarkdownHeaderStubElementType): super(stub, type)

  val level
    get() = calculateHeaderLevel()

  /**
   * Anchor reference to this header.
   * Should be compatible with anchors generated by GitHub.
   *
   * ```markdown
   * # Some header text -> #some-header-text
   * ```
   *
   * Will always return unqiue anchor even if there are mutliple headers with the same text.
   * Example file:
   * ```markdown
   * # Some header -> #some-header
   *
   * # Some header -> #some-header-1
   * ```
   */
  val anchorText: String?
    get() = obtainAnchorText(this)

  val contentElement: MarkdownHeaderContent?
    get() = childrenOfType(MarkdownTokenTypeSets.HEADER_CONTENT).filterIsInstance<MarkdownHeaderContent>().firstOrNull()

  override fun accept(visitor: PsiElementVisitor) {
    when (visitor) {
      is MarkdownElementVisitor -> visitor.visitHeader(this)
      else -> super.accept(visitor)
    }
  }

  override fun getReferences(): Array<PsiReference?> {
    return ReferenceProvidersRegistry.getReferencesFromProviders(this)
  }

  override fun getPresentation(): ItemPresentation {
    val headerText = getHeaderText()
    val text = headerText ?: "Invalid header: $text"
    return object: ColoredItemPresentation {
      override fun getPresentableText(): String {
        val prevSibling = prevSibling
        if (Registry.`is`("markdown.structure.view.list.visibility") && MarkdownTokenTypeSets.LIST_MARKERS.contains(prevSibling.elementType)) {
          return prevSibling.text + text
        }
        return text
      }

      override fun getIcon(open: Boolean): Icon? = null

      override fun getTextAttributesKey(): TextAttributesKey? {
        return when (level) {
          1 -> MarkdownStructureColors.MARKDOWN_HEADER_BOLD
          else -> MarkdownStructureColors.MARKDOWN_HEADER
        }
      }
    }
  }

  override fun getName(): String? {
    return getHeaderText()
  }

  private fun findContentHolder(): PsiElement? {
    return findChildByType(MarkdownTokenTypeSets.INLINE_HOLDING_ELEMENT_TYPES)
  }

  private fun getHeaderText(): String? {
    if (!isValid) {
      return null
    }
    val contentHolder = findContentHolder() ?: return null
    return StringUtil.trim(contentHolder.text)
  }

  /**
   * Builds visible text for this header.
   * Visible text includes text content of all children without starting hash and a whitespace after hash.
   *
   * For a child inline link this method will only take it's visible part ([MarkdownLink.linkText]).
   *
   * @param hideImages - if true, all child images will be ignored,
   *  otherwise images will be included as is, with it's visible and invisible parts
   *  (so rendered result will show actual image).
   */
  @ApiStatus.Experimental
  fun buildVisibleText(hideImages: Boolean = true): String? {
    val contentHolder = findContentHolder() ?: return null
    val builder = StringBuilder()
    val children = contentHolder.children().dropWhile { it.hasType(MarkdownTokenTypeSets.WHITE_SPACES) }
    traverseNameText(builder, children, hideImages)
    return builder.toString().trim(' ')
  }

  private fun traverseNameText(builder: StringBuilder, elements: Sequence<PsiElement>, hideImages: Boolean) {
    for (child in elements) {
      when (child) {
        is LeafPsiElement -> builder.append(child.text)
        is MarkdownInlineLink -> traverseNameText(builder, child.linkText?.contentElements.orEmpty(), hideImages)
        is MarkdownImage -> {
          if (!hideImages) {
            builder.append(child.text)
          }
        }
        else -> traverseNameText(builder, child.children(), hideImages)
      }
    }
  }

  private fun buildRawAnchorText(includeStartingHash: Boolean = false): String? {
    val contentHolder = findContentHolder() ?: return null
    val children = contentHolder.children().dropWhile { it.hasType(MarkdownTokenTypeSets.WHITE_SPACES) }
    val text = buildString {
      if (includeStartingHash) {
        append("#")
      }
      var count = 0
      for (child in children) {
        if (child.hasType(MarkdownTokenTypeSets.WHITE_SPACES)) {
          append(" ")
          continue
        }
        when (child) {
          is MarkdownImage -> append("")
          is MarkdownInlineLink -> processInlineLink(child)
          else -> append(child.text)
        }
        count += 1
      }
    }
    val replaced = replaceEntities(text).lowercase().replace(garbageRegex, "").replace(" ", "-")

    return when {
      AdvancedSettings.getBoolean("markdown.squash.multiple.dashes.in.header.anchors") -> replaced.replace(Regex("-{2,}"), "-")
      else -> replaced
    }
  }

  private fun StringBuilder.processInlineLink(element: MarkdownInlineLink) {
    val contentElements = element.linkText?.contentElements.orEmpty()
    val withoutWhitespaces = contentElements.filterNot { it.hasType(MarkdownTokenTypeSets.WHITE_SPACES) }
    withoutWhitespaces.joinTo(this, separator = " ") {
      when (it) {
        is MarkdownImage -> ""
        else -> it.text
      }
    }
  }

  companion object {
    internal val garbageRegex = Regex("[^\\p{IsAlphabetic}\\d\\-_ ]")

    private fun buildUniqueAnchorText(header: MarkdownHeader): String? {
      val anchorText = obtainRawAnchorText(header) ?: return null
      val number = calculateUniqueNumber(header, anchorText)
      return createUniqueAnchorText(anchorText, number)
    }

    private fun calculateUniqueNumber(header: MarkdownHeader, rawAnchorText: String): Int {
      val file = header.containingFile
      val headers = CachedValuesManager.getCachedValue(file) {
        CachedValueProvider.Result.create(SyntaxTraverser.psiTraverser(file).filterIsInstance<MarkdownHeader>(),
                                          PsiModificationTracker.MODIFICATION_COUNT)
      }
      val sameHeaders = headers.filter { obtainRawAnchorText(it) == rawAnchorText }
      return sameHeaders.takeWhile { it != header }.count()
    }

    @ApiStatus.Internal
    @JvmStatic
    fun createUniqueAnchorText(rawAnchorText: String, occurenceNumber: Int): String {
      return when (occurenceNumber) {
        0 -> rawAnchorText
        else -> "$rawAnchorText-$occurenceNumber"
      }
    }

    fun obtainAnchorText(header: MarkdownHeader): String? {
      return CachedValuesManager.getCachedValue(header) {
        CachedValueProvider.Result.create(buildUniqueAnchorText(header), PsiModificationTracker.MODIFICATION_COUNT)
      }
    }

    private fun obtainRawAnchorText(header: MarkdownHeader): String? {
      return CachedValuesManager.getCachedValue(header) {
        CachedValueProvider.Result.create(header.buildRawAnchorText(false), PsiModificationTracker.MODIFICATION_COUNT)
      }
    }

    private val ENTITY_REGEX = Regex("""&(?:([a-zA-Z0-9]+)|#([0-9]{1,8})|#[xX]([a-fA-F0-9]{1,8}));""")

    fun replaceEntities(text: CharSequence): String {
      return ENTITY_REGEX.replace(text) { match ->
        val g = match.groups
        if (g.size > 4 && g[4] != null) {
          val char = g[4]!!.value[0]
          char.toString()
        } else {
          val code = when {
            g[1] != null -> Entities.map[match.value]
            g[2] != null -> g[2]!!.value.toInt()
            g[3] != null -> g[3]!!.value.toInt(16)
            else -> null
          }
          code?.toChar()?.toString() ?: "&amp;${match.value.substring(1)}"
        }
      }
    }
  }
}
